# -*- coding: utf-8 -*-

''' 分析家数据格式相关类的聚集
    这些类的数据读取方法,在返回值中存在股票代码时,必须已经转换为标准代码

'''


import logging
import re
import glob
import os.path
from datetime import datetime
from struct import unpack

from wolfox.foxit.base.common import Quote,createQuoteFromSrc,createXInfoFromSrc,createReportFromSrc
import wolfox.foxit.source.reader as reader
from wolfox.foxit.source.common import patterns,pattern1,pattern2

logger = logging.getLogger("foxit.source.fxj.extractor")

#老版本的上证指数代码 ==> 6.0版本的代码的对照表
CODEMAP60 = {"SH1A0001":"SH000001","SH1A0002":"SH000002","SH1A0003":"SH000003","SH1B0001":"SH000004",
             "SH1B0002":"SH000005","SH1B0004":"SH000006","SH1B0005":"SH000007","SH1B0006":"SH000008",
             "SH1B0007":"SH000010","SH1B0008":"SH000011","SH1B0009":"SH000012","SH1B0010":"SH000013",
             "SH1C0003":"SH000016"}


class CodeReader(object):
    ''' 代码读取器

        用于获得特定来源的(交易所缩写编号,标准代码,名字,证券类型)元组列表
        这个类的目的是归集相关函数，免予污染模块名字空间
    '''

    def read_codes(self,fname):
        ''' 主函数
            
            返回(交易所缩写编号,标准代码,名字,股票类型)的列表. 其标准code根据特定的对应关系获得，这里是prefix + 源code
        '''
        lines = self.readlines(fname)
        assert 'SuperStk Name Table',lines[0][:-1]  #[:-1]是为了去掉回车
        prefix = lines[1][:-1]
        #name_tuples = self.parse(lines[2:],prefix)
        results = [ self.parse(line,prefix) for line in lines[2:]]
        results = filter(lambda record: record[3],results)
        return results

    def readlines(self,fname):  #已经废弃
        fh = open(fname)
        lines = fh.readlines()
        fh.close()
        return lines

    def parse(self,line,prefix):    #分析家的数据源中,代码文件传入的line是gbk的
        #code,name = line.split('\t')
        code,name = line[:6],line[7:-1].decode('gbk')  #源文件格式为gbk
        code = prefix + code
        if code in CODEMAP60: code = CODEMAP60[code]    #映射
        stype = self.find_type(code)
        return prefix,code,name,stype
        
    def find_type(self,code):
        ms = filter(lambda t: t[0].match(code) and t[1],patterns)   #留下的是正确的pattern
        if(ms):
            return ms[0][1] #必然只有一个元素
        return None


class CatalogReader(object):
    ''' 板块读取器
        
        这个类的目的是归集相关函数，免予污染模块名字空间
    '''

    def read_catalogs(self,directory):
        ''' 从指定目录中读取所有板块文件,并返回(板块名,板块成员)列表
        '''
        extractor = CatalogExtractor()
        creader = reader.Reader(extractor)
        filenames = glob.glob(os.path.join(directory,'*.BLK'))
        catalogs_list = [ creader.read(filename) for filename in filenames ]
        names = [ os.path.splitext(os.path.basename(filename))[0].decode('gbk') for filename in filenames ]
        #这里的decode原因是该板块文件格式本身是gbk的
        return zip(names,catalogs_list)


min5_list = [ '09%02d' % m for m in range(35,60,5)] \
        +   [ '10%02d' % m for m in range(0,60,5)]  \
        +   [ '11%02d' % m for m in range(0,31,5)]  \
        +   [ '13%02d' % m for m in range(5,60,5)]  \
        +   [ '14%02d' % m for m in range(0,60,5)]  \
        +   ['1500']
min5_map = dict(zip(min5_list,range(len(min5_list))))

day_func = lambda tid:int(datetime.fromtimestamp(tid).strftime("%Y%m%d"))
#min5_func = lambda tid:int(datetime.utcfromtimestamp(tid).strftime("%y%m%d%H%M"))   #必须utc,否则时区出错
min5_func = lambda tid:int(datetime.utcfromtimestamp(tid).strftime("%Y%m%d")) * 100 + min5_map[datetime.utcfromtimestamp(tid).strftime("%H%M")]  #正好未溢出


class TradeExtractor(object): #交易数据解析
    """
    分析家 DAD文件格式：
    文件头 + M * (股票名记录 + N * 股票交易记录) + 结尾记录
    文件头(16字节)：
        unsigned int 1: 文件标记 0x33fc198c
        unsigned int 2: 未知
        unsigned int 3: 股票个数
        unsigned int 4: 文件标记 0x0
    
    股票名记录(32字节)：
       unsigned int date_id: 0xffffffff表示类型为股票名字
       char[8] : 股票名字，其中上海以SH开头，深圳以SZ开头，后六位为标准编码
       char[20] : 全零
     
    交易记录(32字节)：
       unsigned int date_id: !=0xffffffff
               为1970-01-01起始的秒数，utc时间，对日判断无影响，但对分钟判断有影响，必须用对函数
       float open_price:开盘价
       float high_price:最高价
       float low_price:最低价
       float close_price:收盘价
       float total_volume:成交量//单位为手
       float total_value:成交金额//单位为元
       unsigned short:未知
       unsigned short:未知
    结尾记录：
        unsigned int date_id: 0xffffffff
        char[28]:全零
    """

    def __init__(self,tfunc=day_func):
        self.tfunc = tfunc

    def handleHeader(self,filehandle,fname): #第三个参数为去掉扩展名和路径的文件名
        header = filehandle.read(16)
        tag,nouse,snumber,nouse = unpack('iiii',header)
        if(tag != 0x33fc198c):
            raise IOError,u'文件类型错误，不是分析家日线文件'
        self.curstock = '000000'

    def extract_old(self,filehandle):
        #此处在60分钟处理月数据时递归溢出
        ds = filehandle.read(4)
        (dateid,) = unpack('i',ds)
        record = None       
        while(dateid == -1):   #名字
            self.curstock = filehandle.read(8)
            #print self.curstock
            if(unpack('ii',self.curstock) == (0,0)): #结束块
                #print "结束块"
                return #返回None
            if(self.curstock in CODEMAP60): #分析家6.0升级后改动了上证指数的编码，需要将老版本的代码改到新版本
                self.curstock = CODEMAP60[self.curstock]
            self.is_index = True if self.curstock[:5] == 'SH000' or self.curstock[:5] == 'SZ399' else False
            filehandle.read(20)
            ds = filehandle.read(4)
            (dateid,) = unpack('i',ds)
        else:#数据
            #tdate = int(datetime.fromtimestamp(dateid).strftime("%Y%m%d"))
            #print 'dateid:%s' % dateid
            tdate = self.tfunc(dateid)
            #print tdate
            rs = filehandle.read(28)
            (open,high,low,close,volume,amount,nouse) = unpack('fffffff',rs)
            if self.is_index:
                volume //= 1000
            record = createQuoteFromSrc(self.curstock,tdate,open,close,high,low,volume,amount)
            print 'record: %s:%s' % (record.tstock,record.tdate)
            if not pattern1.match(record.tstock):   #非目标stockcode,如国债
                return self.extract(filehandle)    #递归调用
            if pattern2.match(record.tstock) and record.tvolume <= 0:
                logger.info(u'问题数据，%s.volume=%s,record=%s' % (record.tstock,record.tvolume,record))
                return self.extract(filehandle)    #递归调用
            if record.tavg == 0:
                record.tavg = (record.thigh + record.tlow +1) // 2 
        return record
                       
    def extract(self,filehandle):
        while(True):
            ds = filehandle.read(4)
            (dateid,) = unpack('i',ds)
            if(dateid == -1):   #名字
                self.curstock = filehandle.read(8)
                #print self.curstock
                if(unpack('ii',self.curstock) == (0,0)): #结束块
                    #print "结束块"
                    return #返回None
                if(self.curstock in CODEMAP60): #分析家6.0升级后改动了上证指数的编码，需要将老版本的代码改到新版本
                    self.curstock = CODEMAP60[self.curstock]
                self.is_index = True if self.curstock[:5] == 'SH000' or self.curstock[:5] == 'SZ399' else False
                filehandle.read(20)
            else:#数据
                #tdate = int(datetime.fromtimestamp(dateid).strftime("%Y%m%d"))
                #print 'dateid:%s' % dateid
                tdate = self.tfunc(dateid)
                #print tdate
                rs = filehandle.read(28)
                (open,high,low,close,volume,amount,nouse) = unpack('fffffff',rs)
                if self.is_index:
                    volume //= 1000
                record = createQuoteFromSrc(self.curstock,tdate,open,close,high,low,volume,amount)
                #print 'record: %s:%s' % (record.tstock,record.tdate)
                if record.tavg == 0:
                    record.tavg = (record.thigh + record.tlow +1) // 2 
                if pattern2.match(record.tstock) and record.tvolume <= 0:
                    logger.warning(u'问题数据，%s.volume=%s,record=%s' % (record.tstock,record.tvolume,record))
                elif pattern1.match(record.tstock):   #排除非目标stockcode,如国债
                    return record    #
                

rday = lambda r:r.tdate/100
rhour = lambda r:r.tdate%100 /12

class M5to60Extractor(object):
    ''' 在extractor层面处理完5=>60的转换，否则需要的改动太大
        不能继承自TradeExtractor, 否则在从TradeExtractor来的对extract的递归调用会使用子类的extract,导致重数太多，语义也不对
    '''

    def __init__(self):
        self.extractor = TradeExtractor(min5_func)

    def handleHeader(self,filehandle,fname): #第三个参数为去掉扩展名和路径的文件名
        self.extractor.handleHeader(filehandle,fname)
        self.curday = -1
        self.curhour = -1
        self.curtrade = Quote()
        self.finished = False

    def extract(self,filehandle):
        if self.finished:
            return None
        record = self.extractor.extract(filehandle)
        #print "record: %s %s" % (record.tstock,record.tdate)        
        while record and record.tstock == self.curtrade.tstock and self.curday == rday(record) and self.curhour == rhour(record):
            #继续累加
            self.curtrade.tclose = record.tclose
            self.curtrade.tamount += record.tamount
            self.curtrade.tvolume += record.tvolume
            if self.curtrade.thigh < record.thigh:
                self.curtrade.thigh = record.thigh
            if self.curtrade.tlow > record.tlow:
                self.curtrade.tlow = record.tlow
            record = self.extractor.extract(filehandle)    #缺乏do..while或until循环语句的不便
            #if record: print "record: %s %s" % (record.tstock,record.tdate)
        else:
            self.curtrade.calcAvg()
            completed_trade = self.curtrade
            completed_trade.tdate = completed_trade.tdate / 100 * 100 + completed_trade.tdate % 100 / 12    #转换为小时计数
            self.curtrade = record
            if not record:
                self.finished = True
            else:
                self.curday = rday(record)
                self.curhour = rhour(record)            
            if completed_trade.tvolume == 0:    #过滤掉第一个桩
                return self.extract(filehandle)
            #print completed_trade.tstock,completed_trade.tdate
            return completed_trade


class XExtractor(object): #除权数据解析
    """
    分析家除权文件http://download.fxj.com.cn/base/SPLIT.PWR
    文件格式
          文件头 + M * (股票名记录 + N * 股票交易记录)
    文件头(8字节)：
        unsigned int 1: 文件标记 0xff43c832
        unsigned int 2: 未知，总是0xffcc83dd
 
    股票名记录(20字节)：
        unsigned int date_id: 0xffffffff表示类型为股票名字
        char[8] : 股票名字，其中上海以SH开头，深圳以SZ开头，后六位为标准编码
        char[8] : 未知
            通常这8个字节分为2个float,是上一个股票最后一条除权记录的配股价格和红利价格字段的拷贝。
            估计是除权文件生成程序的bug，或者起到一个校验作用
           
  
    交易记录(20字节)：
        unsigned int date_id: !=0xffffffff
              为1970-01-01起始的秒数
        float   stock_sggs;		// 每股送股股数，如果是10送5，则为0.5
        float   stock_pggs;		// 每股配股股数，方式同上
        float   stock_pgjg;		// 配股每股价格
        float   stock_bonus;	// 每股红利价格
    """

    __slots__ = "curstock"

    def handleHeader(self,filehandle,fname): #第三个参数为去掉扩展名和路径的文件名,貌似现在没用了
        header = filehandle.read(8)
        tag,nouse = unpack('II',header)
        if(tag != 0xff43c832):
            raise IOError,u'文件类型错误，不是分析家除权文件,tag:%x' % tag
        self.curstock = '000000'

    def extract(self,filehandle):
        ds = filehandle.read(4)
        if(len(ds) == 0):
            return #文件结束，返回None
        (dateid,) = unpack('i',ds)
        while(dateid == -1):   #名字
            self.curstock = filehandle.read(8)
            #print "new stock:",self.curstock
            filehandle.read(8)
            ds = filehandle.read(4)
            (dateid,) = unpack('i',ds)
        else:#数据
            #tdate = int(datetime.fromtimestamp(dateid).strftime("%Y%m%d"))
            tdate = day_func(dateid)
            rs = filehandle.read(16)
            (sgbl,pgbl,pgj,fhbl) = unpack('ffff',rs)
            record = createXInfoFromSrc(self.curstock,tdate,tdate,pgbl,sgbl,fhbl,pgj)
        return record


class ReportExtractor(object):  #财务报表数据解析
    """
      分析家财务文件(FIN)解析
      文件格式：
      	文件头 + N      记录
      文件头(8字节)：
      		标志(4字节)：0x223FD90C
      		保留(4字节)
      记录(166字节)		
       股名(10字节): 市场名(2字节,SH/SZ) + 保留(2字节 0000) + 代码(6字节)
       起始日期    (4字节)Integer      从1970.1.1开始的秒数(未确认,目前是全零)
       更新日期    (4字节)Integer      从1970.1.1开始的秒数 
       总股本      (4字节)	Float       万股
       国家股      (4字节)	Float       万股
       发起法人股  (4字节)	Float       万股
       法人股      (4字节)	Float       万股
       Ｂ股        (4字节)	Float       万股
       Ｈ股        (4字节)	Float       万股
       流通Ａ股    (4字节)	Float       万股
       职工股      (4字节)	Float       万股
       A2转配股    (4字节)	Float       万股
       总资产      (4字节)	Float       千元
       流动资产    (4字节)	Float       千元
       固定资产    (4字节)	Float       千元
       无形资产    (4字节)	Float       千元
       长期投资    (4字节)	Float       千元
       流动负债    (4字节)	Float       千元
       长期负债    (4字节)	Float       千元
       资本公积金  (4字节)	Float       千元
       每股公积金  (4字节)	Float       元/股
       股东权益    (4字节)	Float       千元
       主营收入    (4字节)	Float       千元
       主营利润    (4字节)	Float       千元 
       其它利润    (4字节)	Float       千元
       营业利润    (4字节)	Float       千元(此项未确认)
       投资收益    (4字节)	Float       千元
       补贴收入    (4字节)	Float       千元
       营业外收支  (4字节)	Float       千元(此项未确认，可能错误)
       上年损益调整 (4字节)Float       千元
       利润总额    (4字节)	Float       千元
       税后利润    (4字节)	Float       千元(此项未确认)
       净利润      (4字节)	Float       千元
       未分配利润  (4字节)	Float       千元
       每股未分配  (4字节)	Float       元
       每股收益    (4字节)	Float       元
       每股净资产  (4字节)	Float       元
       调整每股净资产 (4字节)Float     千元
       股东权益比  (4字节)	Float       百分之一
       净资产收益率  (4字节)	Float       百分之一
    """
    
    __slots__ = "curstock"

    def handleHeader(self,filehandle,fname): #第三个参数为去掉扩展名和路径的文件名
        header = filehandle.read(8)
        tag,nouse = unpack('II',header)
        if(tag != 0x223fd90c):
            raise IOError,u'文件类型错误，不是分析家财务文件,tag:%x' % tag
        self.curstock = '000000'

    def extract(self,filehandle):
        sname = filehandle.read(10)
        if(len(sname) == 0):
            return #文件结束，返回None
        self.curstock = sname[:2] + sname[4:]
        tmp = filehandle.read(16)
        (nouse,fdate,zgb,nouse) = unpack('iiff',tmp)
        releaseday = day_func(fdate)   #int(date.fromtimestamp(fdate).strftime("%Y%m%d"))
        filehandle.read(8)
        tmp = filehandle.read(16)
        (bg,hg,ag,nouse) = unpack('ffff',tmp)
        filehandle.read(36)
        tmp = filehandle.read(4)
        (mggjj,) = unpack('f',tmp)
        filehandle.read(52)
        tmp = filehandle.read(24)
        (mgwfplr,mgsy,nouse,mgjzc,nouse,nouse) = unpack('ffffff',tmp)
        record = createReportFromSrc(self.curstock,1,releaseday,releaseday,zgb,bg,hg,ag,mgwfplr,mgsy,mgjzc,mggjj)
        #logger.debug(u"读取财务记录%s,%s" % (self.curstock,releaseday))
        return record


class CatalogExtractor(object): #分析家板块文件(*.blk)解析
    """
        文件结构
            文件头 + N * 股票名记录
        文件头(4字节)
            unsigned int: 板块文件标记 0xff5100a5
        股票名记录(12字节)
            char(8): 股票名
            char(4): 全零
    """

    def handleHeader(self,filehandle,fname): #第三个参数为去掉扩展名和路径的文件名
        header = filehandle.read(4)
        (tag,) = unpack('I',header)
        if(tag != 0xff5100a5):
            raise IOError,u'文件类型错误，不是分析家板块文件,tag:0x%x' % tag

    def extract(self,filehandle):
        sname = filehandle.read(8)
        if(len(sname) == 0):
            return #文件结束，返回None
        filehandle.read(4)  #全零
        return sname
